// SPDX-License-Identifier: GPL-2.0

#define pr_fmt(fmt)  "irq: " fmt

#include <seminix/bug.h>
#include <seminix/of.h>
#include <seminix/of_irq.h>
#include <seminix/slab.h>
#include <seminix/spinlock.h>
#include <seminix/mutex.h>
#include <seminix/irq.h>
#include <seminix/irq/dummychip.h>
#include <seminix/irqdomain.h>
#include <seminix/irqdesc.h>
#include <seminix/irq/chip.h>
#include <seminix/irq/manage.h>

static LIST_HEAD(irq_domain_list);
static DEFINE_MUTEX(irq_domain_mutex);

struct irqchip_fwid {
    struct fwnode_handle	fwnode;
    unsigned int		type;
    char			*name;
    void *data;
};

const struct fwnode_operations irqchip_fwnode_ops;

static struct irq_domain *irq_default_domain;

/**
 * irq_set_default_host() - Set a "default" irq domain
 * @domain: default domain pointer
 *
 * For convenience, it's possible to set a "default" domain that will be used
 * whenever NULL is passed to irq_create_mapping(). It makes life easier for
 * platforms that want to manipulate a few hard coded interrupt numbers that
 * aren't properly represented in the device-tree.
 */
void irq_set_default_host(struct irq_domain *domain)
{
    pr_debug("Default domain set to @0x%p\n", domain);

    irq_default_domain = domain;
}

/**
 * irq_domain_alloc_fwnode - Allocate a fwnode_handle suitable for
 *                           identifying an irq domain
 * @type:	Type of irqchip_fwnode. See linux/irqdomain.h
 * @name:	Optional user provided domain name
 * @id:		Optional user provided id if name != NULL
 * @data:	Optional user-provided data
 *
 * Allocate a struct irqchip_fwid, and return a poiner to the embedded
 * fwnode_handle (or NULL on failure).
 *
 * Note: The types IRQCHIP_FWNODE_NAMED and IRQCHIP_FWNODE_NAMED_ID are
 * solely to transport name information to irqdomain creation code. The
 * node is not stored. For other types the pointer is kept in the irq
 * domain struct.
 */
struct fwnode_handle *__irq_domain_alloc_fwnode(unsigned int type, int id,
                        const char *name, void *data)
{
    struct irqchip_fwid *fwid;
    char *n;

    fwid = kzalloc(sizeof (*fwid), GFP_KERNEL);

    switch (type) {
    case IRQCHIP_FWNODE_NAMED:
        n = kasprintf(GFP_KERNEL, "%s", name);
        break;
    case IRQCHIP_FWNODE_NAMED_ID:
        n = kasprintf(GFP_KERNEL, "%s-%d", name, id);
        break;
    default:
        n = kasprintf(GFP_KERNEL, "irqchip@%p", data);
        break;
    }

    if (!fwid || !n) {
        kfree(fwid);
        kfree(n);
        return NULL;
    }

    fwid->type = type;
    fwid->name = n;
    fwid->data = data;
    fwid->fwnode.ops = &irqchip_fwnode_ops;

    return &fwid->fwnode;
}

/**
 * irq_domain_free_fwnode - Free a non-OF-backed fwnode_handle
 *
 * Free a fwnode_handle allocated with irq_domain_alloc_fwnode.
 */
void irq_domain_free_fwnode(struct fwnode_handle *fwnode)
{
    struct irqchip_fwid *fwid;

    if (WARN_ON(!is_fwnode_irqchip(fwnode)))
        return;

    fwid = container_of(fwnode, struct irqchip_fwid, fwnode);
    kfree(fwid->name);
    kfree(fwid);
}

/**
 * __irq_domain_add() - Allocate a new irq_domain data structure
 * @fwnode: firmware node for the interrupt controller
 * @ops: domain callbacks
 * @host_data: Controller private data pointer
 *
 * Allocates and initialize and irq_domain structure.
 * Returns pointer to IRQ domain, or NULL on failure.
 */
struct irq_domain *__irq_domain_add(struct fwnode_handle *fwnode, const struct irq_domain_ops *ops, void *host_data)
{
    struct device_node *of_node = to_of_node(fwnode);
    struct irqchip_fwid *fwid;
    struct irq_domain *domain;

    static atomic_t unknown_domains;

    domain = kzalloc(sizeof(*domain), GFP_KERNEL);
    if (WARN_ON(!domain))
        return NULL;

    if (fwnode && is_fwnode_irqchip(fwnode)) {
        fwid = container_of(fwnode, struct irqchip_fwid, fwnode);

        switch (fwid->type) {
        case IRQCHIP_FWNODE_NAMED:
        case IRQCHIP_FWNODE_NAMED_ID:
            domain->name = kstrdup(fwid->name, GFP_KERNEL);
            if (!domain->name) {
                kfree(domain);
                return NULL;
            }
            domain->flags |= IRQ_DOMAIN_NAME_ALLOCATED;
            break;
        default:
            domain->fwnode = fwnode;
            domain->name = fwid->name;
            break;
        }
    } else if (of_node) {
        char *name;

        /*
         * DT paths contain '/', which debugfs is legitimately
         * unhappy about. Replace them with ':', which does
         * the trick and is not as offensive as '\'...
         */
        name = kasprintf(GFP_KERNEL, "%pOF", of_node);
        if (!name) {
            kfree(domain);
            return NULL;
        }

        strreplace(name, '/', ':');

        domain->name = name;
        domain->fwnode = fwnode;
        domain->flags |= IRQ_DOMAIN_NAME_ALLOCATED;
    }

    if (!domain->name) {
        if (fwnode)
            pr_err("Invalid fwnode type for irqdomain\n");
        domain->name = kasprintf(GFP_KERNEL, "unknown-%d",
                     atomic_inc_return(&unknown_domains));
        if (!domain->name) {
            kfree(domain);
            return NULL;
        }
        domain->flags |= IRQ_DOMAIN_NAME_ALLOCATED;
    }

    /* Fill structure */
    INIT_RADIX_TREE(&domain->revmap_tree, GFP_KERNEL);
    mutex_init(&domain->revmap_tree_mutex);
    domain->ops = ops;
    domain->host_data = host_data;

    mutex_lock(&irq_domain_mutex);
    list_add(&domain->link, &irq_domain_list);
    mutex_unlock(&irq_domain_mutex);

    pr_debug("Added domain %s\n", domain->name);
    return domain;
}

/**
 * irq_domain_remove() - Remove an irq domain.
 * @domain: domain to remove
 *
 * This routine is used to remove an irq domain. The caller must ensure
 * that all mappings within the domain have been disposed of prior to
 * use, depending on the revmap type.
 */
void irq_domain_remove(struct irq_domain *domain)
{
    mutex_lock(&irq_domain_mutex);

    WARN_ON(!radix_tree_empty(&domain->revmap_tree));

    list_del(&domain->link);

    /*
     * If the going away domain is the default one, reset it.
     */
    if (unlikely(irq_default_domain == domain))
        irq_set_default_host(NULL);

    mutex_unlock(&irq_domain_mutex);

    pr_debug("Removed domain %s\n", domain->name);

    if (domain->flags & IRQ_DOMAIN_NAME_ALLOCATED)
        kfree(domain->name);
    kfree(domain);
}

void irq_domain_update_bus_token(struct irq_domain *domain,
                 enum irq_domain_bus_token bus_token)
{
    char *name;

    if (domain->bus_token == bus_token)
        return;

    mutex_lock(&irq_domain_mutex);

    domain->bus_token = bus_token;

    name = kasprintf(GFP_KERNEL, "%s-%d", domain->name, bus_token);
    if (!name) {
        mutex_unlock(&irq_domain_mutex);
        return;
    }

    if (domain->flags & IRQ_DOMAIN_NAME_ALLOCATED)
        kfree(domain->name);
    else
        domain->flags |= IRQ_DOMAIN_NAME_ALLOCATED;

    domain->name = name;

    mutex_unlock(&irq_domain_mutex);
}

/**
 * irq_domain_get_irq_data - Get irq_data associated with @virq and @domain
 * @domain:	domain to match
 * @virq:	IRQ number to get irq_data
 */
struct irq_data *irq_domain_get_irq_data(struct irq_domain *domain,
                     int virq)
{
    struct irq_data *irq_data;

    for (irq_data = irq_get_irq_data(virq); irq_data;
         irq_data = irq_data->parent_data)
        if (irq_data->domain == domain)
            return irq_data;

    return NULL;
}

/**
 * irq_find_matching_fwspec() - Locates a domain for a given fwspec
 * @fwspec: FW specifier for an interrupt
 * @bus_token: domain-specific data
 */
struct irq_domain *irq_find_matching_fwspec(struct irq_fwspec *fwspec,
                        enum irq_domain_bus_token bus_token)
{
    struct irq_domain *h, *found = NULL;
    struct fwnode_handle *fwnode = fwspec->fwnode;
    int rc;

    /* We might want to match the legacy controller last since
     * it might potentially be set to match all interrupts in
     * the absence of a device node. This isn't a problem so far
     * yet though...
     *
     * bus_token == DOMAIN_BUS_ANY matches any domain, any other
     * values must generate an exact match for the domain to be
     * selected.
     */
    mutex_lock(&irq_domain_mutex);
    list_for_each_entry(h,  &irq_domain_list, link) {
        if (h->ops->select && fwspec->param_count)
            rc = h->ops->select(h, fwspec, bus_token);
        else if (h->ops->match)
            rc = h->ops->match(h, to_of_node(fwnode), bus_token);
        else
            rc = ((fwnode != NULL) && (h->fwnode == fwnode) &&
                  ((bus_token == DOMAIN_BUS_ANY) ||
                   (h->bus_token == bus_token)));

        if (rc) {
            found = h;
            break;
        }
    }
    mutex_unlock(&irq_domain_mutex);
    return found;
}

/**
 * irq_domain_check_msi_remap - Check whether all MSI irq domains implement
 * IRQ remapping
 *
 * Return: false if any MSI irq domain does not support IRQ remapping,
 * true otherwise (including if there is no MSI irq domain)
 */
bool irq_domain_check_msi_remap(void)
{
    struct irq_domain *h;
    bool ret = true;

    mutex_lock(&irq_domain_mutex);
    list_for_each_entry(h, &irq_domain_list, link) {
        if (irq_domain_is_msi(h) &&
            !irq_domain_hierarchical_is_msi_remap(h)) {
            ret = false;
            break;
        }
    }
    mutex_unlock(&irq_domain_mutex);
    return ret;
}

/**
 * irq_domain_hierarchical_is_msi_remap - Check if the domain or any
 * parent has MSI remapping support
 * @domain: domain pointer
 */
bool irq_domain_hierarchical_is_msi_remap(struct irq_domain *domain)
{
    for (; domain; domain = domain->parent) {
        if (irq_domain_is_msi_remap(domain))
            return true;
    }
    return false;
}

static void irq_domain_clear_mapping(struct irq_domain *domain,
                     irq_hw_number_t hwirq)
{
    mutex_lock(&domain->revmap_tree_mutex);
    radix_tree_delete(&domain->revmap_tree, hwirq);
    mutex_unlock(&domain->revmap_tree_mutex);
}

static void irq_domain_set_mapping(struct irq_domain *domain,
                   irq_hw_number_t hwirq,
                   struct irq_data *irq_data)
{
    mutex_lock(&domain->revmap_tree_mutex);
    radix_tree_insert(&domain->revmap_tree, hwirq, irq_data);
    mutex_unlock(&domain->revmap_tree_mutex);
}

int irq_find_mapping(struct irq_domain *domain,
                  irq_hw_number_t hwirq)
{
    struct irq_data *data;

    /* Look for default domain if nececssary */
    if (domain == NULL)
        domain = irq_default_domain;
    if (domain == NULL)
        return 0;

    rcu_read_lock();
    data = radix_tree_lookup(&domain->revmap_tree, hwirq);
    rcu_read_unlock();
    return data ? data->irq : 0;
}

static int irq_domain_translate(struct irq_domain *d,
                struct irq_fwspec *fwspec,
                irq_hw_number_t *hwirq, unsigned int *type)
{
    if (d->ops->translate)
        return d->ops->translate(d, fwspec, hwirq, type);

    /* If domain has no translation, then we assume interrupt line */
    *hwirq = fwspec->param[0];
    return 0;
}

static void of_phandle_args_to_fwspec(struct of_phandle_args *irq_data,
                      struct irq_fwspec *fwspec)
{
    int i;

    fwspec->fwnode = irq_data->np ? &irq_data->np->fwnode : NULL;
    fwspec->param_count = irq_data->args_count;

    for (i = 0; i < irq_data->args_count; i++)
        fwspec->param[i] = irq_data->args[i];
}

int irq_create_fwspec_mapping(struct irq_fwspec *fwspec)
{
    struct irq_domain *domain;
    struct irq_data *irq_data;
    irq_hw_number_t hwirq;
    unsigned int type = IRQ_TYPE_NONE;
    int virq;

    if (fwspec->fwnode) {
        domain = irq_find_matching_fwspec(fwspec, DOMAIN_BUS_WIRED);
        if (!domain)
            domain = irq_find_matching_fwspec(fwspec, DOMAIN_BUS_ANY);
    } else {
        domain = irq_default_domain;
    }

    if (!domain) {
        pr_warn("no irq domain found for %s !\n",
            of_node_full_name(to_of_node(fwspec->fwnode)));
        return 0;
    }

    if (irq_domain_translate(domain, fwspec, &hwirq, &type))
        return 0;

    /*
     * WARN if the irqchip returns a type with bits
     * outside the sense mask set and clear these bits.
     */
    if (WARN_ON(type & ~IRQ_TYPE_SENSE_MASK))
        type &= IRQ_TYPE_SENSE_MASK;

    /*
     * If we've already configured this interrupt,
     * don't do it again, or hell will break loose.
     */
    virq = irq_find_mapping(domain, hwirq);
    if (virq) {
        /*
         * If the trigger type is not specified or matches the
         * current trigger type then we are done so return the
         * interrupt number.
         */
        if (type == IRQ_TYPE_NONE || type == irq_get_trigger_type(virq))
            return virq;

        /*
         * If the trigger type has not been set yet, then set
         * it now and return the interrupt number.
         */
        if (irq_get_trigger_type(virq) == IRQ_TYPE_NONE) {
            irq_data = irq_get_irq_data(virq);
            if (!irq_data)
                return 0;

            irqd_set_trigger_type(irq_data, type);
            return virq;
        }

        pr_warn("type mismatch, failed to map hwirq-%lu for %s!\n",
            hwirq, of_node_full_name(to_of_node(fwspec->fwnode)));
        return 0;
    }

    virq = irq_domain_alloc_irqs(domain, 1, fwspec);
    if (virq <= 0)
        return 0;


    irq_data = irq_get_irq_data(virq);

    /* Store trigger type */
    irqd_set_trigger_type(irq_data, type);

    return virq;
}

int irq_create_of_mapping(struct of_phandle_args *irq_data)
{
    struct irq_fwspec fwspec;

    of_phandle_args_to_fwspec(irq_data, &fwspec);
    return irq_create_fwspec_mapping(&fwspec);
}

/**
 * irq_domain_create_hierarchy - Add a irqdomain into the hierarchy
 * @parent:	Parent irq domain to associate with the new domain
 * @flags:	Irq domain flags associated to the domain
 * @size:	Size of the domain. See below
 * @fwnode:	Optional fwnode of the interrupt controller
 * @ops:	Pointer to the interrupt domain callbacks
 * @host_data:	Controller private data pointer
 *
 * If @size is 0 a tree domain is created, otherwise a linear domain.
 *
 * If successful the parent is associated to the new domain and the
 * domain flags are set.
 * Returns pointer to IRQ domain, or NULL on failure.
 */
struct irq_domain *irq_domain_create_hierarchy(struct irq_domain *parent,
                        u32 flags, struct fwnode_handle *fwnode,
                        const struct irq_domain_ops *ops,
                        void *host_data)
{
    struct irq_domain *domain;

    domain = __irq_domain_add(fwnode, ops, host_data);
    if (domain) {
        domain->parent = parent;
        domain->flags |= flags;
    }

    return domain;
}

static void irq_domain_insert_irq(int virq)
{
    struct irq_data *data;

    for (data = irq_get_irq_data(virq); data; data = data->parent_data) {
        struct irq_domain *domain = data->domain;

        domain->mapcount++;
        irq_domain_set_mapping(domain, data->hwirq, data);

        /* If not already assigned, give the domain the chip's name */
        if (!domain->name && data->chip)
            domain->name = data->chip->name;

        __irqd_clear(data, IRQ_NOREQUEST);
    }
}

static void irq_domain_remove_irq(int virq)
{
    unsigned long flags;
    struct irq_desc *desc = irq_get_desc_lock(virq, &flags, 0);
    struct irq_data *data;

    irq_get_desc_lock(virq, &flags, 0);
    __irqd_set(&desc->irq_data, IRQ_NOREQUEST);
    irq_put_desc_unlock(desc, flags);

    irq_set_chip_and_handler(virq, NULL, NULL);
    synchronize_irq(virq);
    smp_mb();

    for (data = irq_get_irq_data(virq); data; data = data->parent_data) {
        struct irq_domain *domain = data->domain;
        irq_hw_number_t hwirq = data->hwirq;

        domain->mapcount--;
        irq_domain_clear_mapping(domain, hwirq);
    }
}

static struct irq_data *irq_domain_insert_irq_data(struct irq_domain *domain,
        struct irq_data *child)
{
    struct irq_data *irq_data;

    irq_data = kzalloc(sizeof (*irq_data), GFP_KERNEL);
    if (irq_data) {
        child->parent_data = irq_data;
        irq_data->irq = child->irq;
        irq_data->domain = domain;
    }

    return irq_data;
}

static void irq_domain_free_irq_data(int virq, int nr_irqs)
{
    struct irq_data *irq_data, *tmp;
    int i;

    for (i = 0; i < nr_irqs; i++) {
        irq_data = irq_get_irq_data(virq + i);
        tmp = irq_data->parent_data;
        irq_data->parent_data = NULL;
        irq_data->domain = NULL;

        while (tmp) {
            irq_data = tmp;
            tmp = tmp->parent_data;
            kfree(irq_data);
        }
    }
}

static int irq_domain_alloc_irq_data(struct irq_domain *domain,
                        int virq, int nr_irqs)
{
    struct irq_data *irq_data;
    struct irq_domain *parent;
    int i;

    /* The outermost irq_data is embedded in struct irq_desc */
    for (i = 0; i < nr_irqs; i++) {
        irq_data = irq_get_irq_data(virq + i);
        irq_data->domain = domain;

        for (parent = domain->parent; parent; parent = parent->parent) {
            irq_data = irq_domain_insert_irq_data(parent, irq_data);
            if (!irq_data) {
                irq_domain_free_irq_data(virq, i + 1);
                return -ENOMEM;
            }
        }
    }

    return 0;
}

int irq_domain_alloc_irqs_hierarchy(struct irq_domain *domain,
                    int irq_base, int nr_irqs, void *arg)
{
    return domain->ops->alloc(domain, irq_base, nr_irqs, arg);
}

void irq_domain_free_irqs_hierarchy(struct irq_domain *domain,
                       int irq_base, int nr_irqs)
{
    if (domain->ops->free)
        domain->ops->free(domain, irq_base, nr_irqs);
}

int __irq_domain_alloc_irqs(struct irq_domain *domain, int irq_base,
                int nr_irqs, void *arg,
                const struct cpumask *affinity)
{
    int i, ret, virq;

    if (domain == NULL) {
        domain = irq_default_domain;
        if (WARN(!domain, "domain is NULL; cannot allocate IRQ\n"))
            return -EINVAL;
    }

    if (!domain->ops->alloc) {
        pr_debug("domain->ops->alloc() is NULL\n");
        return -ENOSYS;
    }

    virq = -1;
    if (irq_base >= 0)
        virq = irq_base;

    virq = __irq_alloc_descs(virq, nr_irqs, affinity);
    if (virq < 0) {
        pr_debug("cannot allocate IRQ(base %d, count %d)\n",
                irq_base, nr_irqs);
        return virq;
    }

    if (irq_domain_alloc_irq_data(domain, virq, nr_irqs)) {
        pr_debug("cannot allocate memory for IRQ%d\n", virq);
        ret = -ENOMEM;
        goto out_free_desc;
    }

    mutex_lock(&irq_domain_mutex);
    ret = irq_domain_alloc_irqs_hierarchy(domain, virq, nr_irqs, arg);
    if (ret < 0) {
        mutex_unlock(&irq_domain_mutex);
        goto out_free_irq_data;
    }
    for (i = 0; i < nr_irqs; i++)
        irq_domain_insert_irq(virq + i);
    mutex_unlock(&irq_domain_mutex);

    return virq;

out_free_irq_data:
    irq_domain_free_irq_data(virq, nr_irqs);
out_free_desc:
    irq_free_descs(virq, nr_irqs);
    return ret;
}

/**
 * irq_domain_free_irqs - Free IRQ number and associated data structures
 * @virq:	base IRQ number
 * @nr_irqs:	number of IRQs to free
 */
void irq_domain_free_irqs(int virq, int nr_irqs)
{
    struct irq_data *data = irq_get_irq_data(virq);
    int i;

    if (WARN(!data || !data->domain || !data->domain->ops->free,
         "NULL pointer, cannot free irq\n"))
        return;

    mutex_lock(&irq_domain_mutex);
    for (i = 0; i < nr_irqs; i++)
        irq_domain_remove_irq(virq + i);
    irq_domain_free_irqs_hierarchy(data->domain, virq, nr_irqs);
    mutex_unlock(&irq_domain_mutex);

    irq_domain_free_irq_data(virq, nr_irqs);
    irq_free_descs(virq, nr_irqs);
}

/**
 * irq_domain_set_hwirq_and_chip - Set hwirq and irqchip of @virq at @domain
 * @domain:	Interrupt domain to match
 * @virq:	IRQ number
 * @hwirq:	The hwirq number
 * @chip:	The associated interrupt chip
 * @chip_data:	The associated chip data
 */
int irq_domain_set_hwirq_and_chip(struct irq_domain *domain, int virq,
                  irq_hw_number_t hwirq, struct irq_chip *chip,
                  void *chip_data)
{
    struct irq_data *irq_data = irq_domain_get_irq_data(domain, virq);

    if (!irq_data)
        return -ENOENT;

    irq_data->hwirq = hwirq;
    irq_data->chip = chip ? chip : &no_irq_chip;
    irq_data->chip_data = chip_data;

    return 0;
}

/**
 * irq_domain_set_info - Set the complete data for a @virq in @domain
 * @domain:		Interrupt domain to match
 * @virq:		IRQ number
 * @hwirq:		The hardware interrupt number
 * @chip:		The associated interrupt chip
 * @chip_data:		The associated interrupt chip data
 * @handler:		The interrupt flow handler
 * @handler_name:	The interrupt handler name
 */
void irq_domain_set_info(struct irq_domain *domain, int virq,
             irq_hw_number_t hwirq, struct irq_chip *chip,
             void *chip_data, irq_flow_handler_t handler,
             void *handler_data, const char *handler_name)
{
    irq_domain_set_hwirq_and_chip(domain, virq, hwirq, chip, chip_data);
    __irq_set_handler(virq, handler, 0, handler_name);
    irq_set_handler_data(virq, handler_data);
}

/**
 * irq_domain_reset_irq_data - Clear hwirq, chip and chip_data in @irq_data
 * @irq_data:	The pointer to irq_data
 */
void irq_domain_reset_irq_data(struct irq_data *irq_data)
{
    irq_data->hwirq = 0;
    irq_data->chip = &no_irq_chip;
    irq_data->chip_data = NULL;
}
